﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using StyleMVVM.Data.Activation;

namespace StyleMVVM.Data.Transform
{
	internal class CompiledTransformStrategy<TIn, TOut> : ITransformStrategy
	{
		private static MethodInfo deepCloneIntoMethod;
		private static MethodInfo objectGetValueMethod;
		private static MethodInfo objectSetValueMethod;
		private static MethodInfo transformServiceMethod;
		private static MethodInfo createMethod;

		private Func<TIn, ITransformService, IActivationService, IReflectionService, TOut> transformMethod;
		private readonly ITransformMapInfo translationMap;
		private readonly IActivationService activationService;
		private readonly IReflectionService reflectionService;
		private readonly ITransformService transformService;
		private bool internallyLoose = false;

		static CompiledTransformStrategy()
		{
			createMethod = typeof(CompiledTransformStrategyHelper).GetTypeInfo().GetDeclaredMethod("Create");

			deepCloneIntoMethod = typeof(CompiledTransformStrategyHelper).GetTypeInfo()
			                                                             .GetDeclaredMethod("DeepCloneIntoInstance");

			objectGetValueMethod = typeof(CompiledTransformStrategyHelper).GetTypeInfo().GetDeclaredMethod("GetPropertyValue");

			objectSetValueMethod = typeof(CompiledTransformStrategyHelper).GetTypeInfo().GetDeclaredMethod("SetPropertyValue");

			transformServiceMethod = typeof(CompiledTransformStrategyHelper).GetTypeInfo().GetDeclaredMethod("Transform");
		}

		public CompiledTransformStrategy(ITransformService transformService,
		                                 IActivationService cloneService,
		                                 IReflectionService reflectionService,
		                                 ITransformMapInfo translationMap)
		{
			this.translationMap = translationMap;
			this.activationService = cloneService;
			this.reflectionService = reflectionService;
			this.transformService = transformService;
		}

		public object Transform(object valueIn, Type transformHint)
		{
			return transformMethod((TIn)valueIn, transformService, activationService, reflectionService);
		}

		public bool IsLooseTranform
		{
			get
			{
				if (internallyLoose)
				{
					return true;
				}

				return translationMap.IsLooseTransform;
			}
		}

		public void Initalize()
		{
			createCompiledMethod();
		}

		protected virtual void createCompiledMethod()
		{
			ParameterExpression tValueParameter =
				Expression.Parameter(typeof(TIn), "tValue");

			ParameterExpression transformServiceParameter =
				Expression.Parameter(typeof(ITransformService), "transformService");

			ParameterExpression activationServiceParameter =
				Expression.Parameter(typeof(IActivationService), "activationService");

			ParameterExpression reflectionServiceParameter =
				Expression.Parameter(typeof(IReflectionService), "reflectionService");

			ParameterExpression returnValueVar =
				Expression.Variable(typeof(TOut), "returnValue");

			MethodInfo closedCreateMethod = createMethod.MakeGenericMethod(typeof(TOut));

			Expression assignNewToReturnValue =
				Expression.Assign(returnValueVar,
				                  Expression.Call(
					                  null,
					                  closedCreateMethod,
					                  activationServiceParameter));

			List<Expression> tranformStatements = new List<Expression> { assignNewToReturnValue };

			foreach (ITransformPropertyMapInfo propertyMap in translationMap.MappedProperties())
			{
				Expression propertyAssignStatement =
					createAssignPropertyStatement(propertyMap,
					                              transformServiceParameter,
					                              activationServiceParameter,
					                              reflectionServiceParameter,
					                              tValueParameter,
					                              returnValueVar);

				if (propertyAssignStatement != null)
				{
					tranformStatements.Add(propertyAssignStatement);
				}
			}

			tranformStatements.Add(returnValueVar);

			BlockExpression body =
				Expression.Block(new[] { returnValueVar }, tranformStatements);

			transformMethod =
				Expression.Lambda<Func<TIn, ITransformService, IActivationService, IReflectionService, TOut>>(
					body,
					tValueParameter,
					transformServiceParameter,
					activationServiceParameter,
					reflectionServiceParameter).Compile();
		}

		protected virtual Expression createAssignPropertyStatement(
			ITransformPropertyMapInfo propertyMap,
			ParameterExpression transformServiceParameter,
			ParameterExpression activationServiceParameter,
			ParameterExpression reflectionServiceParameter,
			ParameterExpression tValueParameter,
			ParameterExpression returnValueVar)
		{
			if (propertyMap.PropertyType == propertyMap.TransformType)
			{
				return
					createAssignPropertyStatementForSameType(
						propertyMap,
						activationServiceParameter,
						reflectionServiceParameter,
						tValueParameter,
						returnValueVar);
			}

			return
				createAssignPropertyStatementForDiffType(
					propertyMap,
					transformServiceParameter,
					activationServiceParameter,
					reflectionServiceParameter,
					tValueParameter,
					returnValueVar);
		}

		protected virtual Expression createAssignPropertyStatementForSameType(
			ITransformPropertyMapInfo propertyMap,
			ParameterExpression activationServiceParameter,
			ParameterExpression reflectionServiceParameter,
			ParameterExpression tValueParameter,
			ParameterExpression returnValueVar)
		{
			Expression returnExpression = null;

			if (propertyMap.PropertyType.GetTypeInfo().IsPrimitive ||
			    propertyMap.PropertyType == typeof(string))
			{
				Expression getProprtyExpression =
					createGetExpressionForProperty(propertyMap.PropertyName, reflectionServiceParameter, tValueParameter);

				returnExpression =
					createSetExpressionForProperty(propertyMap.ToPropertyName,
					                               reflectionServiceParameter,
					                               returnValueVar,
					                               getProprtyExpression);
			}
			else
			{
				MethodInfo closedDeepClone =
					deepCloneIntoMethod.MakeGenericMethod(propertyMap.PropertyType);

				Expression getProprtyExpression =
					createGetExpressionForProperty(propertyMap.PropertyName, reflectionServiceParameter, tValueParameter);

				Expression getClonePropertyExpression =
					createGetExpressionForProperty(propertyMap.ToPropertyName, reflectionServiceParameter, returnValueVar);

				Expression callExpression =
					Expression.Call(null,
					                closedDeepClone,
					                activationServiceParameter,
					                getProprtyExpression,
					                getClonePropertyExpression);

				returnExpression =
					createSetExpressionForProperty(propertyMap.ToPropertyName,
					                               reflectionServiceParameter,
					                               returnValueVar,
					                               callExpression);
			}

			return returnExpression;
		}

		protected virtual Expression createAssignPropertyStatementForDiffType(
			ITransformPropertyMapInfo propertyMap,
			ParameterExpression transformServiceParameter,
			ParameterExpression activationServiceParameter,
			ParameterExpression reflectionServiceParameter,
			ParameterExpression tValueParameter,
			ParameterExpression returnValueVar)
		{
			Expression returnExpression = null;

			Expression getProprtyExpression =
				createGetExpressionForProperty(propertyMap.PropertyName,
				                               reflectionServiceParameter,
				                               tValueParameter);

			MethodInfo closedTransformMethod =
				transformServiceMethod.MakeGenericMethod(propertyMap.PropertyType, propertyMap.TransformType);

			Expression transformStatement =
				Expression.Call(null,
				                closedTransformMethod,
				                transformServiceParameter,
				                Expression.Convert(getProprtyExpression, propertyMap.PropertyType),
				                Expression.Constant(false),
				                Expression.Constant(true));

			returnExpression =
				createSetExpressionForProperty(propertyMap.ToPropertyName,
				                               reflectionServiceParameter,
				                               returnValueVar,
				                               Expression.Convert(transformStatement, propertyMap.TransformType));

			return returnExpression;
		}

		protected Expression createGetExpressionForProperty(string propertyName,
		                                                    ParameterExpression reflectionServiceParameter,
		                                                    ParameterExpression valueParameter)
		{
			Expression returnValue = null;

			if (propertyName.IndexOf('.') == -1)
			{
				returnValue = Expression.Property(valueParameter, propertyName);
			}
			else
			{
				MethodInfo closedGetMethod = objectGetValueMethod.MakeGenericMethod();

				returnValue = Expression.Call(null,
				                              closedGetMethod,
				                              reflectionServiceParameter,
				                              valueParameter,
				                              Expression.Constant(propertyName));
			}

			return returnValue;
		}

		protected Expression createSetExpressionForProperty(string propertyName,
		                                                    ParameterExpression reflectionServiceParameter,
		                                                    ParameterExpression valueParameter,
		                                                    Expression setValue)
		{
			Expression returnValue = null;

			if (propertyName.IndexOf('.') == -1)
			{
				returnValue =
					Expression.Assign(
						Expression.Property(valueParameter, propertyName),
						setValue);
			}
			else
			{
				returnValue = Expression.Call(
					null,
					objectSetValueMethod,
					reflectionServiceParameter,
					valueParameter,
					Expression.Constant(propertyName),
					setValue);
			}

			return returnValue;
		}
	}
}